Заказчик
Приложение Procrastinate Pro+
Цель проекта
На основе данных, предоставленных компанией, необходимо провести анализ и ответить на вопросы:
Входные данные
Данные о пользователях, привлечённых с 1 мая по 27 октября 2019 года:
лог сервера с данными об их посещениях (файл visits_info_short.csv)
User Id
— уникальный идентификатор пользователя;
Region
— страна пользователя;
Device
— тип устройства пользователя;
Channel
— идентификатор источника перехода;
Session Start
— дата и время начала сессии;
Session End
— дата и время окончания сессии.
выгрузка их покупок за этот период (файл orders_info_short.csv)
User Id
— уникальный идентификатор пользователя;
Event Dt
— дата и время покупки;
Revenue
— сумма заказа.
рекламные расходы (файл costs_info_short.csv)
Channel
— идентификатор рекламного источника;
Dt
— дата проведения рекламной кампании;
Costs
— расходы на эту кампанию.
Ход исследования
Исследование пройдёт в четыре этапа:
import time
import warnings
import pandas as pd
import numpy as np
import seaborn as sns
import plotly.express as px
import matplotlib.pyplot as plt
%matplotlib inline
from datetime import datetime
from datetime import date, timedelta as td
from datetime import datetime, timedelta
pd.set_option('display.max_columns', None)
warnings.filterwarnings(action='ignore')
try:
visits, orders, costs = (
pd.read_csv('/datasets/visits_info_short.csv'),
pd.read_csv('/datasets/orders_info_short.csv'),
pd.read_csv('/datasets/costs_info_short.csv')
)
except:
visits, orders, costs = (
pd.read_csv('visits_info_short.csv'),
pd.read_csv('orders_info_short.csv'),
pd.read_csv('costs_info_short.csv')
)
РАЗВЕДОЧНЫЙ АНАЛИЗ ДАННЫХ
— Обзор датафрейма visits
display(visits.info(),visits.head(), visits.tail())
<class 'pandas.core.frame.DataFrame'> RangeIndex: 309901 entries, 0 to 309900 Data columns (total 6 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 User Id 309901 non-null int64 1 Region 309901 non-null object 2 Device 309901 non-null object 3 Channel 309901 non-null object 4 Session Start 309901 non-null object 5 Session End 309901 non-null object dtypes: int64(1), object(5) memory usage: 14.2+ MB
None
User Id | Region | Device | Channel | Session Start | Session End | |
---|---|---|---|---|---|---|
0 | 981449118918 | United States | iPhone | organic | 2019-05-01 02:36:01 | 2019-05-01 02:45:01 |
1 | 278965908054 | United States | iPhone | organic | 2019-05-01 04:46:31 | 2019-05-01 04:47:35 |
2 | 590706206550 | United States | Mac | organic | 2019-05-01 14:09:25 | 2019-05-01 15:32:08 |
3 | 326433527971 | United States | Android | TipTop | 2019-05-01 00:29:59 | 2019-05-01 00:54:25 |
4 | 349773784594 | United States | Mac | organic | 2019-05-01 03:33:35 | 2019-05-01 03:57:40 |
User Id | Region | Device | Channel | Session Start | Session End | |
---|---|---|---|---|---|---|
309896 | 329994900775 | UK | PC | LeapBob | 2019-10-31 13:28:12 | 2019-10-31 14:39:29 |
309897 | 334903592310 | France | PC | lambdaMediaAds | 2019-10-31 22:14:52 | 2019-10-31 22:39:36 |
309898 | 540102010126 | Germany | PC | organic | 2019-10-31 01:40:48 | 2019-10-31 01:41:31 |
309899 | 308736936846 | Germany | Mac | organic | 2019-10-31 07:37:34 | 2019-10-31 07:37:55 |
309900 | 109329042535 | Germany | PC | lambdaMediaAds | 2019-10-31 14:17:43 | 2019-10-31 15:17:04 |
— Обзор датафрейма orders
display(orders.info(),orders.head(), orders.tail())
<class 'pandas.core.frame.DataFrame'> RangeIndex: 40212 entries, 0 to 40211 Data columns (total 3 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 User Id 40212 non-null int64 1 Event Dt 40212 non-null object 2 Revenue 40212 non-null float64 dtypes: float64(1), int64(1), object(1) memory usage: 942.6+ KB
None
User Id | Event Dt | Revenue | |
---|---|---|---|
0 | 188246423999 | 2019-05-01 23:09:52 | 4.99 |
1 | 174361394180 | 2019-05-01 12:24:04 | 4.99 |
2 | 529610067795 | 2019-05-01 11:34:04 | 4.99 |
3 | 319939546352 | 2019-05-01 15:34:40 | 4.99 |
4 | 366000285810 | 2019-05-01 13:59:51 | 4.99 |
User Id | Event Dt | Revenue | |
---|---|---|---|
40207 | 651604369137 | 2019-10-31 16:19:07 | 4.99 |
40208 | 275341387049 | 2019-10-31 01:17:17 | 4.99 |
40209 | 374656616484 | 2019-10-31 06:17:29 | 4.99 |
40210 | 168548862926 | 2019-10-31 22:46:19 | 4.99 |
40211 | 329994900775 | 2019-10-31 13:29:06 | 4.99 |
— Обзор датафрейма costs
display(costs.info(), costs.head(), costs.tail())
<class 'pandas.core.frame.DataFrame'> RangeIndex: 1800 entries, 0 to 1799 Data columns (total 3 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 dt 1800 non-null object 1 Channel 1800 non-null object 2 costs 1800 non-null float64 dtypes: float64(1), object(2) memory usage: 42.3+ KB
None
dt | Channel | costs | |
---|---|---|---|
0 | 2019-05-01 | FaceBoom | 113.3 |
1 | 2019-05-02 | FaceBoom | 78.1 |
2 | 2019-05-03 | FaceBoom | 85.8 |
3 | 2019-05-04 | FaceBoom | 136.4 |
4 | 2019-05-05 | FaceBoom | 122.1 |
dt | Channel | costs | |
---|---|---|---|
1795 | 2019-10-23 | lambdaMediaAds | 4.0 |
1796 | 2019-10-24 | lambdaMediaAds | 6.4 |
1797 | 2019-10-25 | lambdaMediaAds | 8.8 |
1798 | 2019-10-26 | lambdaMediaAds | 8.8 |
1799 | 2019-10-27 | lambdaMediaAds | 12.0 |
print('Количество выявленных дубликатов в датафрейме visits:', visits.duplicated().sum())
print('Количество выявленных дубликатов в датафрейме orders:', orders.duplicated().sum())
print('Количество выявленных дубликатов в датафрейме costs:', costs.duplicated().sum())
Количество выявленных дубликатов в датафрейме visits: 0 Количество выявленных дубликатов в датафрейме orders: 0 Количество выявленных дубликатов в датафрейме costs: 0
display(visits.isna().sum(), '""' * 55,
orders.isna().sum(), '""' * 55,
costs.isna().sum())
User Id 0 Region 0 Device 0 Channel 0 Session Start 0 Session End 0 dtype: int64
'""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""'
User Id 0 Event Dt 0 Revenue 0 dtype: int64
'""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""'
dt 0 Channel 0 costs 0 dtype: int64
Промежуточный вывод:
# Переименуем колонки и преобразуем их в соответствии хорошего стиля
visits.columns = [name.lower().replace(' ', '_') for name in visits.columns]
orders.columns = [name.lower().replace(' ', '_') for name in orders.columns]
costs.columns = [name.lower().replace(' ', '_') for name in costs.columns]
# Изменим тип данных колонок с датами
visits.session_start = pd.to_datetime(visits.session_start)
visits.session_end = pd.to_datetime(visits.session_end)
orders.event_dt = pd.to_datetime(orders.event_dt)
costs.dt = pd.to_datetime(costs.dt).dt.date
Познакомимся с данными ближе
visits
Комментарий:
Предлагаю изучить сколько в среднем сессий приходится на одного пользователя за месяц: для этого необходимо разделить число сессий на количество пользователей за месяц - для этого создадим новые колонки с группированными значениями месяца и года.
Количество сессий соответствует числу идентификаторов пользователя.
visits['session_year'] = visits.session_start.dt.year
visits['session_month'] = visits.session_start.dt.month
visits.head()
user_id | region | device | channel | session_start | session_end | session_year | session_month | |
---|---|---|---|---|---|---|---|---|
0 | 981449118918 | United States | iPhone | organic | 2019-05-01 02:36:01 | 2019-05-01 02:45:01 | 2019 | 5 |
1 | 278965908054 | United States | iPhone | organic | 2019-05-01 04:46:31 | 2019-05-01 04:47:35 | 2019 | 5 |
2 | 590706206550 | United States | Mac | organic | 2019-05-01 14:09:25 | 2019-05-01 15:32:08 | 2019 | 5 |
3 | 326433527971 | United States | Android | TipTop | 2019-05-01 00:29:59 | 2019-05-01 00:54:25 | 2019 | 5 |
4 | 349773784594 | United States | Mac | organic | 2019-05-01 03:33:35 | 2019-05-01 03:57:40 | 2019 | 5 |
# Найдем количество сессий и количество пользователей
sessions_per_user = visits.groupby(['session_year', 'session_month']) \
.agg({'user_id':['count', 'nunique']})
# Переименуем колонки
sessions_per_user.columns = ['n_sessions', 'n_users']
# Делим число сессий на количество пользователей
sessions_per_user['sessions_per_user'] = sessions_per_user.n_sessions / sessions_per_user.n_users
sessions_per_user.style.background_gradient(sns.light_palette("brown", as_cmap=True))
n_sessions | n_users | sessions_per_user | ||
---|---|---|---|---|
session_year | session_month | |||
2019 | 5 | 45765 | 27111 | 1.688060 |
6 | 44623 | 26042 | 1.713501 | |
7 | 47970 | 26672 | 1.798515 | |
8 | 55996 | 31064 | 1.802601 | |
9 | 56628 | 31871 | 1.776788 | |
10 | 58919 | 31175 | 1.889944 |
Комментарий:
Мы видим, что количество уникальных пользователей растет в течение исследуемого периода, как и количество сессий. Но нельзя сказать, что две сессии в месяц в среднем на каждого уникального пользователя - это много для развлекательного приложения.
Посмотрим на среднюю продолжительность сессии (ASL).
visits['session_duration_sec'] = (visits.session_end - visits.session_start).dt.seconds
print(f'Среднее значение: {visits.session_duration_sec.mean()} секунд')
print(f'Медианное значение: {visits.session_duration_sec.median()} секунд')
# строим гистограмму
visits.session_duration_sec.hist(bins=50);
plt.title('Распределение средней продолжительности сессии')
plt.ylabel('Визиты')
plt.xlabel('Секунды')
plt.show()
Среднее значение: 1800.0908677287264 секунд Медианное значение: 1244.0 секунд
Промежуточный вывод:
Секунды, проведённые пользователями в приложении, имеют экспоненциальное распределение с пиком в районе нуля.
И это нормально — в любом цифровом сервисе очень многие сессии заканчиваются техническими ошибками.
Средняя сессия длится в районе 30-ти минут, медианная - примерно 21 минут.
orders
# Просмотрим уникальные значения колонки Revenue
orders.revenue.value_counts()
4.99 38631 5.99 780 9.99 385 49.99 212 19.99 204 Name: revenue, dtype: int64
Промежуточный вывод:
Как мы видим, речь идет о пяти продуктах с фиксированной ценой.
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(16, 6))
ax1.plot(orders.pivot_table(index = orders.event_dt.astype('datetime64[D]'), \
values = 'revenue', \
aggfunc = 'sum'))
ax1.grid(True)
ax1.set_title('Распределение выручки по дням')
ax1.set_ylabel('сумма выручки');
ax2.plot(orders.pivot_table(index = orders.event_dt.astype('datetime64[D]'), \
values = 'revenue', \
aggfunc = 'count'),color='orange')
ax2.grid(True)
ax2.set_title('Распределение количества покупок по дням')
ax2.set_ylabel('количество покупок')
plt.show();
Комментарий:
С выручкой, как и с количеством покупок в приложении, все в порядке: мы видим динамику устойчивого роста.
costs
Посмотрим на маркетинговые затраты в разрезе каналов привлечения за исследуемый период
costs.pivot_table(index='channel', values='costs', aggfunc='sum') \
.sort_values(by='costs', ascending=False) \
.reset_index() \
.style.background_gradient(sns.light_palette("brown", as_cmap=True))
channel | costs | |
---|---|---|
0 | TipTop | 54751.300000 |
1 | FaceBoom | 32445.600000 |
2 | WahooNetBanner | 5151.000000 |
3 | AdNonSense | 3911.250000 |
4 | OppleCreativeMedia | 2151.250000 |
5 | RocketSuperAds | 1833.000000 |
6 | LeapBob | 1797.600000 |
7 | lambdaMediaAds | 1557.600000 |
8 | MediaTornado | 954.480000 |
9 | YRabbit | 944.220000 |
Комментарий:
Мы видим, что компания не скупится на маркетинговые расходы, имеется несколько каналов привлечения пользователей.
Основные затраты идут на рекламу в TipTop и FaceBoom.
Разрешается использовать функции, с которыми вы познакомились в теоретических уроках.
Это функции для вычисления значений метрик:
get_profiles()
— для создания профилей пользователей,get_retention()
— для подсчёта Retention Rate,get_conversion()
— для подсчёта конверсии,get_ltv()
— для подсчёта LTV.А также функции для построения графиков:
filter_data()
— для сглаживания данных,plot_retention()
— для построения графика Retention Rate,plot_conversion()
— для построения графика конверсии,plot_ltv_roi
— для визуализации LTV и ROI.# функция для создания пользовательских профилей
def get_profiles(sessions, orders, ad_costs):
# находим параметры первых посещений
profiles = (
sessions.sort_values(by=['user_id', 'session_start'])
.groupby('user_id')
.agg(
{
'session_start': 'first',
'channel': 'first',
'device': 'first',
'region': 'first',
}
)
.rename(columns={'session_start': 'first_ts'})
.reset_index()
)
# для когортного анализа определяем дату первого посещения
# и первый день месяца, в который это посещение произошло
profiles['dt'] = profiles['first_ts'].dt.date
profiles['month'] = profiles['first_ts'].astype('datetime64[M]')
# добавляем признак платящих пользователей
profiles['payer'] = profiles['user_id'].isin(orders['user_id'].unique())
# считаем количество уникальных пользователей
# с одинаковыми источником и датой привлечения
new_users = (
profiles.groupby(['dt', 'channel'])
.agg({'user_id': 'nunique'})
.rename(columns={'user_id': 'unique_users'})
.reset_index()
)
# объединяем траты на рекламу и число привлечённых пользователей
ad_costs = ad_costs.merge(new_users, on=['dt', 'channel'], how='left')
# делим рекламные расходы на число привлечённых пользователей
ad_costs['acquisition_cost'] = ad_costs['costs'] / ad_costs['unique_users']
# добавляем стоимость привлечения в профили
profiles = profiles.merge(
ad_costs[['dt', 'channel', 'acquisition_cost']],
on=['dt', 'channel'],
how='left',
)
# стоимость привлечения органических пользователей равна нулю
profiles['acquisition_cost'] = profiles['acquisition_cost'].fillna(0)
return profiles
# функция для расчёта удержания
def get_retention(
profiles,
sessions,
observation_date,
horizon_days,
dimensions=[],
ignore_horizon=False,
):
# добавляем столбец payer в передаваемый dimensions список
dimensions = ['payer'] + dimensions
# исключаем пользователей, не «доживших» до горизонта анализа
last_suitable_acquisition_date = observation_date
if not ignore_horizon:
last_suitable_acquisition_date = observation_date - timedelta(
days=horizon_days - 1
)
result_raw = profiles.query('dt <= @last_suitable_acquisition_date')
# собираем «сырые» данные для расчёта удержания
result_raw = result_raw.merge(
sessions[['user_id', 'session_start']], on='user_id', how='left'
)
result_raw['lifetime'] = (
result_raw['session_start'] - result_raw['first_ts']
).dt.days
# функция для группировки таблицы по желаемым признакам
def group_by_dimensions(df, dims, horizon_days):
result = df.pivot_table(
index=dims, columns='lifetime', values='user_id', aggfunc='nunique'
)
cohort_sizes = (
df.groupby(dims)
.agg({'user_id': 'nunique'})
.rename(columns={'user_id': 'cohort_size'})
)
result = cohort_sizes.merge(result, on=dims, how='left').fillna(0)
result = result.div(result['cohort_size'], axis=0)
result = result[['cohort_size'] + list(range(horizon_days))]
result['cohort_size'] = cohort_sizes
return result
# получаем таблицу удержания
result_grouped = group_by_dimensions(result_raw, dimensions, horizon_days)
# получаем таблицу динамики удержания
result_in_time = group_by_dimensions(
result_raw, dimensions + ['dt'], horizon_days
)
# возвращаем обе таблицы и сырые данные
return result_raw, result_grouped, result_in_time
# функция для расчёта конверсии
def get_conversion(
profiles,
purchases,
observation_date,
horizon_days,
dimensions=[],
ignore_horizon=False,
):
# исключаем пользователей, не «доживших» до горизонта анализа
last_suitable_acquisition_date = observation_date
if not ignore_horizon:
last_suitable_acquisition_date = observation_date - timedelta(
days=horizon_days - 1
)
result_raw = profiles.query('dt <= @last_suitable_acquisition_date')
# определяем дату и время первой покупки для каждого пользователя
first_purchases = (
purchases.sort_values(by=['user_id', 'event_dt'])
.groupby('user_id')
.agg({'event_dt': 'first'})
.reset_index()
)
# добавляем данные о покупках в профили
result_raw = result_raw.merge(
first_purchases[['user_id', 'event_dt']], on='user_id', how='left'
)
# рассчитываем лайфтайм для каждой покупки
result_raw['lifetime'] = (
result_raw['event_dt'] - result_raw['first_ts']
).dt.days
# группируем по cohort, если в dimensions ничего нет
if len(dimensions) == 0:
result_raw['cohort'] = 'All users'
dimensions = dimensions + ['cohort']
# функция для группировки таблицы по желаемым признакам
def group_by_dimensions(df, dims, horizon_days):
result = df.pivot_table(
index=dims, columns='lifetime', values='user_id', aggfunc='nunique'
)
result = result.fillna(0).cumsum(axis = 1)
cohort_sizes = (
df.groupby(dims)
.agg({'user_id': 'nunique'})
.rename(columns={'user_id': 'cohort_size'})
)
result = cohort_sizes.merge(result, on=dims, how='left').fillna(0)
# делим каждую «ячейку» в строке на размер когорты
# и получаем conversion rate
result = result.div(result['cohort_size'], axis=0)
result = result[['cohort_size'] + list(range(horizon_days))]
result['cohort_size'] = cohort_sizes
return result
# получаем таблицу конверсии
result_grouped = group_by_dimensions(result_raw, dimensions, horizon_days)
# для таблицы динамики конверсии убираем 'cohort' из dimensions
if 'cohort' in dimensions:
dimensions = []
# получаем таблицу динамики конверсии
result_in_time = group_by_dimensions(
result_raw, dimensions + ['dt'], horizon_days
)
# возвращаем обе таблицы и сырые данные
return result_raw, result_grouped, result_in_time
# функция для расчёта LTV и ROI
def get_ltv(
profiles,
purchases,
observation_date,
horizon_days,
dimensions=[],
ignore_horizon=False,
):
# исключаем пользователей, не «доживших» до горизонта анализа
last_suitable_acquisition_date = observation_date
if not ignore_horizon:
last_suitable_acquisition_date = observation_date - timedelta(
days=horizon_days - 1
)
result_raw = profiles.query('dt <= @last_suitable_acquisition_date')
# добавляем данные о покупках в профили
result_raw = result_raw.merge(
purchases[['user_id', 'event_dt', 'revenue']], on='user_id', how='left'
)
# рассчитываем лайфтайм пользователя для каждой покупки
result_raw['lifetime'] = (
result_raw['event_dt'] - result_raw['first_ts']
).dt.days
# группируем по cohort, если в dimensions ничего нет
if len(dimensions) == 0:
result_raw['cohort'] = 'All users'
dimensions = dimensions + ['cohort']
# функция группировки по желаемым признакам
def group_by_dimensions(df, dims, horizon_days):
# строим «треугольную» таблицу выручки
result = df.pivot_table(
index=dims, columns='lifetime', values='revenue', aggfunc='sum'
)
# находим сумму выручки с накоплением
result = result.fillna(0).cumsum(axis=1)
# вычисляем размеры когорт
cohort_sizes = (
df.groupby(dims)
.agg({'user_id': 'nunique'})
.rename(columns={'user_id': 'cohort_size'})
)
# объединяем размеры когорт и таблицу выручки
result = cohort_sizes.merge(result, on=dims, how='left').fillna(0)
# считаем LTV: делим каждую «ячейку» в строке на размер когорты
result = result.div(result['cohort_size'], axis=0)
# исключаем все лайфтаймы, превышающие горизонт анализа
result = result[['cohort_size'] + list(range(horizon_days))]
# восстанавливаем размеры когорт
result['cohort_size'] = cohort_sizes
# собираем датафрейм с данными пользователей и значениями CAC,
# добавляя параметры из dimensions
cac = df[['user_id', 'acquisition_cost'] + dims].drop_duplicates()
# считаем средний CAC по параметрам из dimensions
cac = (
cac.groupby(dims)
.agg({'acquisition_cost': 'mean'})
.rename(columns={'acquisition_cost': 'cac'})
)
# считаем ROI: делим LTV на CAC
roi = result.div(cac['cac'], axis=0)
# удаляем строки с бесконечным ROI
roi = roi[~roi['cohort_size'].isin([np.inf])]
# восстанавливаем размеры когорт в таблице ROI
roi['cohort_size'] = cohort_sizes
# добавляем CAC в таблицу ROI
roi['cac'] = cac['cac']
# в финальной таблице оставляем размеры когорт, CAC
# и ROI в лайфтаймы, не превышающие горизонт анализа
roi = roi[['cohort_size', 'cac'] + list(range(horizon_days))]
# возвращаем таблицы LTV и ROI
return result, roi
# получаем таблицы LTV и ROI
result_grouped, roi_grouped = group_by_dimensions(
result_raw, dimensions, horizon_days
)
# для таблиц динамики убираем 'cohort' из dimensions
if 'cohort' in dimensions:
dimensions = []
# получаем таблицы динамики LTV и ROI
result_in_time, roi_in_time = group_by_dimensions(
result_raw, dimensions + ['dt'], horizon_days
)
return (
result_raw, # сырые данные
result_grouped, # таблица LTV
result_in_time, # таблица динамики LTV
roi_grouped, # таблица ROI
roi_in_time, # таблица динамики ROI
)
def filter_data(df, window):
# для каждого столбца применяем скользящее среднее
for column in df.columns.values:
df[column] = df[column].rolling(window).mean()
return df
# функция для визуализации LTV и ROI
def plot_ltv_roi(ltv, ltv_history, roi, roi_history, horizon, window=7):
# задаём сетку отрисовки графиков
plt.figure(figsize=(20, 20))
# из таблицы ltv исключаем размеры когорт
ltv = ltv.drop(columns=['cohort_size'])
# в таблице динамики ltv оставляем только нужный лайфтайм
ltv_history = ltv_history.drop(columns=['cohort_size'])[[horizon - 1]]
# стоимость привлечения запишем в отдельный фрейм
cac_history = roi_history[['cac']]
# из таблицы roi исключаем размеры когорт и cac
roi = roi.drop(columns=['cohort_size', 'cac'])
# в таблице динамики roi оставляем только нужный лайфтайм
roi_history = roi_history.drop(columns=['cohort_size', 'cac'])[
[horizon - 1]
]
# первый график — кривые ltv
ax1 = plt.subplot(3, 2, 1)
ltv.T.plot(grid=True, ax=ax1)
plt.legend()
plt.xlabel('Лайфтайм')
plt.title('LTV')
# второй график — динамика ltv
ax2 = plt.subplot(3, 2, 2, sharey=ax1)
# столбцами сводной таблицы станут все столбцы индекса, кроме даты
columns = [name for name in ltv_history.index.names if name not in ['dt']]
filtered_data = ltv_history.pivot_table(
index='dt', columns=columns, values=horizon - 1, aggfunc='mean'
)
filter_data(filtered_data, window).plot(grid=True, ax=ax2)
plt.xlabel('Дата привлечения')
plt.title('Динамика LTV пользователей на {}-й день'.format(horizon))
# третий график — динамика cac
ax3 = plt.subplot(3, 2, 3, sharey=ax1)
# столбцами сводной таблицы станут все столбцы индекса, кроме даты
columns = [name for name in cac_history.index.names if name not in ['dt']]
filtered_data = cac_history.pivot_table(
index='dt', columns=columns, values='cac', aggfunc='mean'
)
filter_data(filtered_data, window).plot(grid=True, ax=ax3)
plt.xlabel('Дата привлечения')
plt.title('Динамика стоимости привлечения пользователей')
# четвёртый график — кривые roi
ax4 = plt.subplot(3, 2, 4)
roi.T.plot(grid=True, ax=ax4)
plt.axhline(y=1, color='red', linestyle='--', label='Уровень окупаемости')
plt.legend()
plt.xlabel('Лайфтайм')
plt.title('ROI')
# пятый график — динамика roi
ax5 = plt.subplot(3, 2, 5, sharey=ax4)
# столбцами сводной таблицы станут все столбцы индекса, кроме даты
columns = [name for name in roi_history.index.names if name not in ['dt']]
filtered_data = roi_history.pivot_table(
index='dt', columns=columns, values=horizon - 1, aggfunc='mean'
)
filter_data(filtered_data, window).plot(grid=True, ax=ax5)
plt.axhline(y=1, color='red', linestyle='--', label='Уровень окупаемости')
plt.xlabel('Дата привлечения')
plt.title('Динамика ROI пользователей на {}-й день'.format(horizon))
plt.tight_layout()
plt.show()
# функция для визуализации конверси
def plot_conversion(conversion, conversion_history, horizon, window=7):
# задаём размер сетки для графиков
plt.figure(figsize=(15, 5))
# исключаем размеры когорт
conversion = conversion.drop(columns=['cohort_size'])
# в таблице динамики оставляем только нужный лайфтайм
conversion_history = conversion_history.drop(columns=['cohort_size'])[
[horizon - 1]
]
# первый график — кривые конверсии
ax1 = plt.subplot(1, 2, 1)
conversion.T.plot(grid=True, ax=ax1)
plt.legend()
plt.xlabel('Лайфтайм')
plt.title('Конверсия пользователей')
# второй график — динамика конверсии
ax2 = plt.subplot(1, 2, 2, sharey=ax1)
columns = [
# столбцами сводной таблицы станут все столбцы индекса, кроме даты
name for name in conversion_history.index.names if name not in ['dt']
]
filtered_data = conversion_history.pivot_table(
index='dt', columns=columns, values=horizon - 1, aggfunc='mean'
)
filter_data(filtered_data, window).plot(grid=True, ax=ax2)
plt.xlabel('Дата привлечения')
plt.title('Динамика конверсии пользователей на {}-й день'.format(horizon))
plt.tight_layout()
plt.show()
# функция для визуализации удержания
def plot_retention(retention, retention_history, horizon, window=7):
# задаём размер сетки для графиков
plt.figure(figsize=(15, 10))
# исключаем размеры когорт и удержание первого дня
retention = retention.drop(columns=['cohort_size', 0])
# в таблице динамики оставляем только нужный лайфтайм
retention_history = retention_history.drop(columns=['cohort_size'])[
[horizon - 1]
]
# если в индексах таблицы удержания только payer,
# добавляем второй признак — cohort
if retention.index.nlevels == 1:
retention['cohort'] = 'All users'
retention = retention.reset_index().set_index(['cohort', 'payer'])
# в таблице графиков — два столбца и две строки, четыре ячейки
# в первой строим кривые удержания платящих пользователей
ax1 = plt.subplot(2, 2, 1)
retention.query('payer == True').droplevel('payer').T.plot(
grid=True, ax=ax1
)
plt.legend()
plt.xlabel('Лайфтайм')
plt.title('Удержание платящих пользователей')
# во второй ячейке строим кривые удержания неплатящих
# вертикальная ось — от графика из первой ячейки
ax2 = plt.subplot(2, 2, 2, sharey=ax1)
retention.query('payer == False').droplevel('payer').T.plot(
grid=True, ax=ax2
)
plt.legend()
plt.xlabel('Лайфтайм')
plt.title('Удержание неплатящих пользователей')
# в третьей ячейке — динамика удержания платящих
ax3 = plt.subplot(2, 2, 3)
# получаем названия столбцов для сводной таблицы
columns = [
name
for name in retention_history.index.names
if name not in ['dt', 'payer']
]
# фильтруем данные и строим график
filtered_data = retention_history.query('payer == True').pivot_table(
index='dt', columns=columns, values=horizon - 1, aggfunc='mean'
)
filter_data(filtered_data, window).plot(grid=True, ax=ax3)
plt.xlabel('Дата привлечения')
plt.title(
'Динамика удержания платящих пользователей на {}-й день'.format(
horizon
)
)
# в чётвертой ячейке — динамика удержания неплатящих
ax4 = plt.subplot(2, 2, 4, sharey=ax3)
# фильтруем данные и строим график
filtered_data = retention_history.query('payer == False').pivot_table(
index='dt', columns=columns, values=horizon - 1, aggfunc='mean'
)
filter_data(filtered_data, window).plot(grid=True, ax=ax4)
plt.xlabel('Дата привлечения')
plt.title(
'Динамика удержания неплатящих пользователей на {}-й день'.format(
horizon
)
)
plt.tight_layout()
plt.show()
После каждого пункта сформулируйте выводы.
Комментарий:
Создадим пользовательский профиль посетителей приложения. Для этого используем функцию get_profile
и передадим ей данные о посещениях, покупках и тратах на рекламу.
profiles = get_profiles(visits, orders, costs)
display(profiles.head(), profiles.tail())
user_id | first_ts | channel | device | region | dt | month | payer | acquisition_cost | |
---|---|---|---|---|---|---|---|---|---|
0 | 599326 | 2019-05-07 20:58:57 | FaceBoom | Mac | United States | 2019-05-07 | 2019-05-07 20:58:57 | True | 1.088172 |
1 | 4919697 | 2019-07-09 12:46:07 | FaceBoom | iPhone | United States | 2019-07-09 | 2019-07-09 12:46:07 | False | 1.107237 |
2 | 6085896 | 2019-10-01 09:58:33 | organic | iPhone | France | 2019-10-01 | 2019-10-01 09:58:33 | False | 0.000000 |
3 | 22593348 | 2019-08-22 21:35:48 | AdNonSense | PC | Germany | 2019-08-22 | 2019-08-22 21:35:48 | False | 0.988235 |
4 | 31989216 | 2019-10-02 00:07:44 | YRabbit | iPhone | United States | 2019-10-02 | 2019-10-02 00:07:44 | False | 0.230769 |
user_id | first_ts | channel | device | region | dt | month | payer | acquisition_cost | |
---|---|---|---|---|---|---|---|---|---|
150003 | 999956196527 | 2019-09-28 08:33:02 | TipTop | iPhone | United States | 2019-09-28 | 2019-09-28 08:33:02 | False | 3.500000 |
150004 | 999975439887 | 2019-10-21 00:35:17 | organic | PC | UK | 2019-10-21 | 2019-10-21 00:35:17 | False | 0.000000 |
150005 | 999976332130 | 2019-07-23 02:57:06 | TipTop | iPhone | United States | 2019-07-23 | 2019-07-23 02:57:06 | False | 2.600000 |
150006 | 999979924135 | 2019-09-28 21:28:09 | MediaTornado | PC | United States | 2019-09-28 | 2019-09-28 21:28:09 | False | 0.205714 |
150007 | 999999563947 | 2019-10-18 19:57:25 | organic | iPhone | United States | 2019-10-18 | 2019-10-18 19:57:25 | False | 0.000000 |
Комментарий:
Установим момент и горизонт анализа данных. Согласно заданию, анализ необходимо провести по сосотоянию на 1 ноября 2019 годаб но т.к. наша выгрузка содержит данные по 27 октября 2019, возьмем эту дату. Также, в бизнес-плане заложено, что пользователи должны окупаться не позднее, чем через две недели после привлечения.
# момент анализа
observation_date = profiles.dt.max()
# горизонт анализа
analysis_horizon = 14
print(f'Минимальная дата привлечения пользователей {profiles.dt.min()}')
print(f'Максимальная дата привлечения пользователей {profiles.dt.max()}')
Минимальная дата привлечения пользователей 2019-05-01 Максимальная дата привлечения пользователей 2019-10-27
Комментарий:
Выясним, из каких стран пользователи приходят в приложение и на какую страну приходится больше всего платящих пользователей. Построим таблицу, отражающую количество пользователей и долю платящих из каждой страны и визуализируем ее.
country = profiles.pivot_table(index = 'region', columns = 'payer', values = 'user_id', aggfunc = 'count') \
.rename(columns={True: 'payer', False: 'not_payer'}) \
.sort_values('payer', ascending=False)
fig, ax = plt.subplots(figsize=(18, 3))
country[['payer', 'not_payer']].plot(kind='barh', stacked=True, ax=ax, color=['orange', 'brown'])
ax.legend(bbox_to_anchor = (1.0, 1.0))
ax.set_xlabel('Количство пользователей')
ax.set_ylabel(' ')
ax.set_title('Привлеченные пользователи в разрезе по странам')
plt.show()
country['payer_share'] = round((country.payer / (country.payer + country.not_payer) * 100), 2)
country.reset_index().style.background_gradient(sns.light_palette("brown", as_cmap=True))
payer | region | not_payer | payer | payer_share |
---|---|---|---|---|
0 | United States | 93100 | 6902 | 6.900000 |
1 | UK | 16875 | 700 | 3.980000 |
2 | France | 16787 | 663 | 3.800000 |
3 | Germany | 14365 | 616 | 4.110000 |
Промежуточный вывод:
Однозначно видно, что подавляющее большинство привлеченных пользователей - из США. Также эта страна лидирует и по количеству платящих пользователей. Великобритания и Франция практически на одном уровне по показателям, а аутсайдером даного "рейтинга" является Германия - но при этом по доле плятящих пользователей она на втором месте.
Узнаем, какими устройствами пользуются клиенты и какие устройства предпочитают платящие пользователи. Построим таблицу, отражающую количество пользователей и долю платящих для каждого устройства и визуализируем ее
device = profiles.pivot_table(index = 'device', columns = 'payer', values = 'user_id', aggfunc = 'count') \
.rename(columns={True: 'payer', False: 'not_payer'}) \
.sort_values('payer', ascending=False)
fig, ax = plt.subplots(figsize=(18, 3))
device[['payer', 'not_payer']].plot(kind='barh', stacked=True, ax=ax, color=['orange', 'brown'])
ax.legend(bbox_to_anchor = (1.0, 1.0))
ax.set_xlabel('Количство пользователей')
ax.set_ylabel(' ')
ax.set_title('Привлеченные пользователи в разрезе по странам')
plt.show()
device['payer_share'] = round((device.payer / (device.payer + device.not_payer) * 100), 2)
device.reset_index().style.background_gradient(sns.light_palette("brown", as_cmap=True))
payer | device | not_payer | payer | payer_share |
---|---|---|---|---|
0 | iPhone | 51097 | 3382 | 6.210000 |
1 | Android | 32982 | 2050 | 5.850000 |
2 | Mac | 28130 | 1912 | 6.360000 |
3 | PC | 28918 | 1537 | 5.050000 |
Промежуточный вывод:
Тут все также очевидно, клиенты (платящие и нет) предпочитают пользоваться продуктами Apple
Изучим рекламные источники привлечения и определим каналы, из которых пришло больше всего платящих пользователей. Построим таблицу, отражающую количество пользователей и долю платящих для каждого канала привлечения и визуализируем ее.
channel = profiles.pivot_table(index = 'channel', columns = 'payer', values = 'user_id', aggfunc = 'count') \
.rename(columns={True: 'payer', False: 'not_payer'}) \
.sort_values('payer', ascending=False)
fig, ax = plt.subplots(figsize=(18, 7))
channel[['payer', 'not_payer']].plot(kind='barh', stacked=True, ax=ax, color=['orange', 'brown'])
ax.legend(bbox_to_anchor = (1.0, 1.0))
ax.set_xlabel('Количство пользователей')
ax.set_ylabel(' ')
ax.set_title('Привлеченные пользователи в разрезе по странам')
plt.show()
channel['payer_share'] = round((channel.payer / (channel.payer + channel.not_payer) * 100), 2)
channel.reset_index().style.background_gradient(sns.light_palette("brown", as_cmap=True))
payer | channel | not_payer | payer | payer_share |
---|---|---|---|---|
0 | FaceBoom | 25587 | 3557 | 12.200000 |
1 | TipTop | 17683 | 1878 | 9.600000 |
2 | organic | 55279 | 1160 | 2.060000 |
3 | WahooNetBanner | 8100 | 453 | 5.300000 |
4 | AdNonSense | 3440 | 440 | 11.340000 |
5 | RocketSuperAds | 4096 | 352 | 7.910000 |
6 | LeapBob | 8291 | 262 | 3.060000 |
7 | OppleCreativeMedia | 8372 | 233 | 2.710000 |
8 | lambdaMediaAds | 1924 | 225 | 10.470000 |
9 | YRabbit | 4147 | 165 | 3.830000 |
10 | MediaTornado | 4208 | 156 | 3.570000 |
Промежуточный вывод:
Больше всего пользуются приложением *органические* пользователи, правда их доля конверсия очень низкая. Два самых больших канала: TipTop, FaceBoom - они показывают весьма неплохие результаты. Также хороший поток привлеченных платящих пользователей показывают AdNonSense и RocketSuperAds.
Вывод:
Мы посмотрели на базовые значения конверсии пользователей по стране, устройству и каналу привлечения: очевидными лидерами являются пользователи из Соединенных штатов и при этом большая часть пользователей заходит с мобильных устройств (наибольшую конверсию имеют пользователи, которые пользуются продауктами Apple). Также исследованы рекламные источники привлечения и определены каналы, из которых пришло больше всего платящих пользователей, так лучший канал привлечения который показывает лучшую конверсию - это FaceBoom
Напишите промежуточные выводы.
Посчитаем общую сумму расходов на маркетинг
print(f'Общая сумма затрат на рекламу {costs.costs.sum().round()}')
Общая сумма затрат на рекламу 105497.0
cpt = costs.pivot_table(index='channel', values='costs', aggfunc='sum').sort_values('costs', ascending=False) \
.reset_index()
cpt['%'] = cpt.costs / cpt.costs.sum() * 100
cpt.style.background_gradient(sns.light_palette("brown", as_cmap=True))
channel | costs | % | |
---|---|---|---|
0 | TipTop | 54751.300000 | 51.898295 |
1 | FaceBoom | 32445.600000 | 30.754910 |
2 | WahooNetBanner | 5151.000000 | 4.882589 |
3 | AdNonSense | 3911.250000 | 3.707441 |
4 | OppleCreativeMedia | 2151.250000 | 2.039152 |
5 | RocketSuperAds | 1833.000000 | 1.737485 |
6 | LeapBob | 1797.600000 | 1.703930 |
7 | lambdaMediaAds | 1557.600000 | 1.476436 |
8 | MediaTornado | 954.480000 | 0.904744 |
9 | YRabbit | 944.220000 | 0.895018 |
Промежуточный вывод:
Общая сумма расходов на маркетинг: 105497 y.e.
Больше всего было потрачено на маркетинг в каналах TipTop и FaceBoom. На них приходится больше 80% всех затрат.
fig = px.line(costs.pivot_table(index='dt', values='costs', aggfunc='sum', columns='channel'),
template='plotly_white', title='Затраты на привлечение пользователей в разрезе каналов',
labels=dict(dt="дата", value="затраты на привлечение в день", channel="каналы"))
fig.show()
Промежуточный вывод:
Видим уведичение расходов на маркетинг с течением времени. В конце сентября - начале октября они достигли исторического пика в 630 у.е., после чего стали немного снижаться до 400, а затем к концу октября внось стремились к 600.
costs.dt = pd.to_datetime(costs.dt)
costs['week'] = costs.dt.dt.week
costs['month'] = costs.dt.dt.month
plt.figure(figsize=(25, 7))
# задаем недельные расходы
report_week = costs.pivot_table(index='channel', columns='week', values='costs', aggfunc='sum'
)
# Строим динамику расходов по Неделям
report_week.T.plot(
grid=True, xticks=list(report_week.columns.values), ax=plt.subplot(1, 2, 1)
)
plt.title('Динамика расходов по неделям')
# задаем расходы по месяцам
report_month = costs.pivot_table(index='channel', columns='month', values='costs', aggfunc='sum')
# строим изменение расходов по месяцам
report_month.T.plot(
grid=True, xticks=list(report_month.columns.values), ax=plt.subplot(1, 2, 2)
)
plt.title('Динамика расходов по месяцам')
plt.show()
Промежуточный вывод:
Начиная с 21-й недели началось увеличение расходов в источниках TipTop и FaceBoom. Пик расходов зафиксирован на 39-й неделе в Сентябре (9-й месяц). В остальных источниках увеличения рекламного бюджета не наблюдается
Рассчитаем средний CAC на одного пользователя для всего проекта, используя профили пользователей, но без канала Organic, т.к. этот канал привлечения приложению ничего не стоит.
# Рассчитаем САС на одного пользователя
cac = profiles.query('channel != "organic"')[['user_id', 'acquisition_cost']] \
.drop_duplicates().agg({'acquisition_cost':'mean'})
cac
acquisition_cost 1.127481 dtype: float64
# Рассчитаем САС для каждого источника трафика
cac_channel = profiles.pivot_table(index='channel', values='acquisition_cost', aggfunc='mean') \
.sort_values(by='acquisition_cost', ascending=False) \
.rename(columns={'acquisition_cost':'cac'}) \
.reset_index().style.background_gradient(sns.light_palette("brown", as_cmap=True))
cac_channel
channel | cac | |
---|---|---|
0 | TipTop | 2.799003 |
1 | FaceBoom | 1.113286 |
2 | AdNonSense | 1.008054 |
3 | lambdaMediaAds | 0.724802 |
4 | WahooNetBanner | 0.602245 |
5 | RocketSuperAds | 0.412095 |
6 | OppleCreativeMedia | 0.250000 |
7 | YRabbit | 0.218975 |
8 | MediaTornado | 0.218717 |
9 | LeapBob | 0.210172 |
10 | organic | 0.000000 |
Промежуточный вывод:
Как вы видим, дороже всего приложению обходится пользователь, которого привлекли просредством TipTop (средняя стоимость - 2.80 за пользователя), далее идет FaceBoom и AdNonSense примерно в равной ценовой категории (1.11 и 1.01 соответственно). Органический трафик бесплатен, остальные каналы берут за пользователя от 0.21 до 0.72 у.е
Вывод:
Всего на маркетинг потрачено 105497.3 у.е.
Выяснено, как траты распределены по источникам. Больше всего трат приходитя на источник TipTop (51.9%). Следом идет источник FaceBoom(30.75%). Далее идут низкозатратные источники WahooNetBanner (4.88%), AdNonSense (3.71%), OppleCreativeMedia (2.04%). На оставшиеся источники приходится 1-2% трат.
Визуализировано изменение метрик во времени. Заметны два "активных игрока" - это TipTop и FaceBoom. Больше всего трафика закупали у TipTop, и к октябрю число трат приближалось к 630 у.е. Число трат на FaceBoom находится в районе 300. Остальные каналы привлечения обходятся довольно дешево.
Среди рекламных источников помимо TipTop и FaceBoom с июня начали расти траты на WahooNetBanner (к октябрю их число было в районе 53 у.е.). Затраты на AdNonSense, наоборот, стали снижаться с июня 2019 года (с 55 до 35). По остальным источникам траты варьируются в пределах 3-20 у.е.
Выяснено, сколько в среднем стоило привлечение одного пользователя из каждого источника.
Рассчитан средний CAC на одного пользователя для всего проекта и для каждого источника трафика. Медианная стоимость привлечения одного пользователя - 0.24. При этом среднее находится на значении 0.7. Вероятно, в какие-то периоды мы тратили на пользователей больше, и в данных есть какие-то выбросы, которые влияют на разницу в стреднем и медианой. Максимальная стоимость привлечения пользователя - 3.71 у.е.
На разницу между медианой и средним оказывает влияние большое число нулевой стоимости пользователей. Вероятно, это органический трафик.
Самый дорогой трафик - TipTop (средняя стоимость - 2.80 за пользователя), далее идет FaceBoom и AdNonSense примерно в равной ценовой категории (1.11 и 1.01 соответственно). Органический трафик бесплатен, остальные каналы берут за пользователя от 0.21 до 0.72 у.е.
Используя графики LTV, ROI и CAC, проанализируйте окупаемость рекламы. Считайте, что на календаре 1 ноября 2019 года, а в бизнес-плане заложено, что пользователи должны окупаться не позднее чем через две недели после привлечения. Необходимость включения в анализ органических пользователей определите самостоятельно.
Напишите вывод, опишите возможные причины обнаруженных проблем и промежуточные рекомендации для рекламного отдела.
Для начала оценим общую картину - рассмотрим общую окупаемость рекламы. Для этого построим графики LTV и ROI, а также графики динамики LTV, CAC и ROI с использованием функций get_ltv
для расчетов и plot_ltv_roi
- для визуализации, а так же def filter_data
для сглаживания графиков.
# Исключаем всех "органическсих" пользователей
profiles = profiles.query('channel != "organic"')
# считаем LTV и ROI
ltv_raw, ltv_grouped, ltv_history, roi_grouped, roi_history = get_ltv(profiles, orders, \
observation_date, analysis_horizon)
# строим графики
plot_ltv_roi(ltv_grouped, ltv_history, roi_grouped, roi_history, analysis_horizon)
Промежуточный вывод:
По графикам можно сделать следующие выводы:
Окупаемость рекламы с разбивкой по устройствам
dimensions = ['device']
ltv_raw, ltv_grouped, ltv_history, roi_grouped, roi_history = get_ltv(profiles, orders, observation_date, \
analysis_horizon, dimensions=dimensions)
plot_ltv_roi(ltv_grouped, ltv_history, roi_grouped, roi_history, analysis_horizon, window=14)
Комментарий:
Пользователи всех устройств окупались до июня, а потом что-то пошло не так, особенно для владельцев Mac и Iphone. К сентябрю (и какое-то время в августе) ROI пользователей PC на 14 день был на грани окупаемости, ROI всех остальных устройств был уже ниже уровня начиная с июня. LTV пользователей всё так же подвержен сезонности, но стабилен. Стоимость привлечения пользователей PC самая низкая, а ROI - наоборот, выше остальных. В целом, видим, что стоимость привлечения пользователей всех устройств росла за рассматриваемый период.
Направить внимание следует на владельцев Mac и iPhone - стоимость их привлечения высока (и растет от месяца к месяцу), а окупаемоcть страдает.
# посчитаем и визуализируем конверсию, вызвав функции get_conversion() и plot_conversion()
# смотрим конверсию с разбивкой по устройствам
# смотрим конверсию с разбивкой по устройствам
dimensions = ['device']
conversion_raw, conversion_grouped, conversion_history = get_conversion(profiles, orders, observation_date, \
analysis_horizon, dimensions=dimensions)
plot_conversion(conversion_grouped, conversion_history, analysis_horizon)
Комментарий:
Все устройства хорошо конверсируются, в лидерах устройства производста компании Apple, из общего потока немного выбиваются привлеченные пользователи, использующие PC: они привлекаются не так удачно.
# смотрим удержание с разбивкой по устройствам
retention_raw, retention_grouped, retention_history = get_retention(
profiles, visits, observation_date, analysis_horizon, dimensions=dimensions
)
plot_retention(retention_grouped, retention_history, analysis_horizon)
Промежуточный вывод:
Не видно, чтобы пользователи каких-то определенных устройств особо плохо удерживались. Неплатящие пользователи удерживаются стабильно плохо, а из платящих лучше всего удерживаются пользователи PC, хуже - владельцы iPhone и Mac.
Окупаемость рекламы с разбивкой по странам
dimensions = ['region']
ltv_raw, ltv_grouped, ltv_history, roi_grouped, roi_history = get_ltv(profiles, orders, observation_date, \
analysis_horizon, dimensions=dimensions)
plot_ltv_roi(ltv_grouped, ltv_history, roi_grouped, roi_history, analysis_horizon, window=14)
Комментарий:
Aкцентируем внимание на пользователей из США.
# смотрим конверсию с разбивкой по странам
dimensions = ['region']
conversion_raw, conversion_grouped, conversion_history = get_conversion(
profiles, orders, observation_date, analysis_horizon, dimensions=dimensions
)
plot_conversion(conversion_grouped, conversion_history, analysis_horizon)
Комментарий:
Слабая конверсия у пользователей из Англии, Германии и Франции. Но и их доля относительно всех пользователей не такая большая.
# смотрим удержание с разбивкой по странам
retention_raw, retention_grouped, retention_history = get_retention(profiles, visits, observation_date, \
analysis_horizon, dimensions=dimensions)
plot_retention(retention_grouped, retention_history, analysis_horizon)
Комментарий:
Видим, что пользователи из США явно плохо удерживаются.
Окупаемость рекламы с разбивкой по рекламным каналам.
dimensions = ['channel']
ltv_raw, ltv_grouped, ltv_history, roi_grouped, roi_history = get_ltv(profiles, orders, observation_date, \
analysis_horizon, dimensions=dimensions)
plot_ltv_roi(ltv_grouped, ltv_history, roi_grouped, roi_history, analysis_horizon, window=14)
Комментарий:
Видим большой рост стоимости привлечения пользователей из рекламного канала TipTop. Достаточно много средств было потрачено на эту рекламную сеть,но реальные результаты ниже ожидаемых. Не окупаются пользователи, привлеченные из каналов AdNonSense, FaceBoom, TipTop. Хорошо показала себя рекламная сеть YRabbit в июле - ROI пользователей на 14 день был высок.
# смотрим конверсию с разбивкой по каналам привлечения
dimensions = ['channel']
conversion_raw, conversion_grouped, conversion_history = get_conversion(
profiles, orders, observation_date, analysis_horizon, dimensions=dimensions
)
plot_conversion(conversion_grouped, conversion_history, analysis_horizon)
Комментарий:
Выше всего конверсия у пользователей, привлеченных посредством FaceBoom. Но вообще, динамики у всех пользователей в течение двухнедельного лайфтайма практически и нет.
# смотрим удержание с разбивкой по каналам привлечения
retention_raw, retention_grouped, retention_history = get_retention(
profiles, visits, observation_date, analysis_horizon, dimensions=dimensions
)
plot_retention(retention_grouped, retention_history, analysis_horizon)
Промежуточный вывод:
Хуже всего из платящих удерживаются пользователи из FaceBoom и AdNonSense.
Итак, в результате анализа окупаемости рекламы можно сделать следующие выводы:
Вывод и рекомендация:
Компания-заказчик представлена на рынке Европы (Германия, Франция и Великобритания) и в США. Несмотря на огромные вложения в рекламу, последние несколько месяцев компания терпит убытки.
Мы проанализировали данные, предоставленные компанией-заказчиком (информация о пользователях, привлечённых с 1 мая по 27 октября 2019 года):
В результате исследования было выявлено, что основной причиной финансовых проблем являются рекламные траты на привлечение пользователей посредством FaceBoom, TipTop в США и AdNonSence в Европе (топ3 САС):
высокая стоимость привлечения у этих каналов, с горизонтом событий в две недели она не окупается;
при этом, платящие пользователи FaceBoom и AdNonSence очень плохо удерживаются;
рекламная система TipTop оказалась неэффективной, расходы на привлечение в TipTop за полгода выросли почти в три раза;
пользователи Mac и IPhone оказались самыми неплатящими, возможно есть проблема с оптимизацией приложения Procrastinate Pro+ для ios устройств;
и летом 2019 произошло какое-то событие в США, которое кардинально повлияло на поведение пользователей.
Рекомендация